// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package main

import (
	"bytes"
	"fmt"
	exec "internal/execabs"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"golang.org/x/mod/semver"
)

// Dir通过指定
// 预期的导入路径和文件系统目录。
type Dir struct {
	importPath string // 导入该目录的路径
	dir        string // 文件系统目录
	inModule   bool
}

// Dirs是用于扫描目录树的结构。
// 它的Next方法返回它找到的下一个Go源目录。
// 虽然它可以用来多次扫描树，但是
// 只遍历树一次，缓存它找到的数据。
type Dirs struct {
	scan   chan Dir // 由walk生成的目录。
	hist   []Dir    // 报告的DIR的历史。
	offset int      // 下一个的柜台。
}

var dirs Dirs

// dirsInit开始扫描GOROOT和GOPATH中的包目录。任何
// 传递给它的额外路径包含在通道中。
func dirsInit(extra ...Dir) {
	dirs.hist = make([]Dir, 0, 1000)
	dirs.hist = append(dirs.hist, extra...)
	dirs.scan = make(chan Dir)
	go dirs.walk(codeRoots())
}

// 重置将扫描放回起始位置。
func (d *Dirs) Reset() {
	d.offset = 0
}

// Next返回扫描中的下一个目录。布尔值
// 当扫描完成时为false。
func (d *Dirs) Next() (Dir, bool) {
	if d.offset < len(d.hist) {
		dir := d.hist[d.offset]
		d.offset++
		return dir, true
	}
	dir, ok := <-d.scan
	if !ok {
		return Dir{}, false
	}
	d.hist = append(d.hist, dir)
	d.offset++
	return dir, ok
}

// 漫步在戈鲁特和戈帕斯的树林中。
func (d *Dirs) walk(roots []Dir) {
	for _, root := range roots {
		d.bfsWalkRoot(root)
	}
	close(d.scan)
}

// bfsWalkRoot以广度优先的词汇顺序遍历单个目录层次结构。
// 它找到的每个Go源目录都在d.scan上交付。
func (d *Dirs) bfsWalkRoot(root Dir) {
	root.dir = filepath.Clean(root.dir) // 因为filepath.Join无论如何都会这样做

	// 这是在此过程中要检查的目录队列。
	this := []string{}
	// 下一步是下一步要检查的目录队列。
	next := []string{root.dir}

	for len(next) > 0 {
		this, next = next, this[0:0]
		for _, dir := range this {
			fd, err := os.Open(dir)
			if err != nil {
				log.Print(err)
				continue
			}
			entries, err := fd.Readdir(0)
			fd.Close()
			if err != nil {
				log.Print(err)
				continue
			}
			hasGoFiles := false
			for _, entry := range entries {
				name := entry.Name()
				// 对于普通文件，请记住此目录是否包含任何.go
				// 源文件，否则忽略它们。
				if !entry.IsDir() {
					if !hasGoFiles && strings.HasSuffix(name, ".go") {
						hasGoFiles = true
					}
					continue
				}
				// 条目是一个目录。

				// go工具会忽略以.、u或“testdata”开头的目录。
				if name[0] == '.' || name[0] == '_' || name == "testdata" {
					continue
				}
				// 在模块中时，忽略供应商目录并在模块边界处停止。
				if root.inModule {
					if name == "vendor" {
						continue
					}
					if fi, err := os.Stat(filepath.Join(dir, name, "go.mod")); err == nil && !fi.IsDir() {
						continue
					}
				}
				// 记住这个（完全限定的）目录，以便下次通过。
				next = append(next, filepath.Join(dir, name))
			}
			if hasGoFiles {
				// 这是一个候选人。
				importPath := root.importPath
				if len(dir) > len(root.dir) {
					if importPath != "" {
						importPath += "/"
					}
					importPath += filepath.ToSlash(dir[len(root.dir)+1:])
				}
				d.scan <- Dir{importPath, dir, root.inModule}
			}
		}

	}
}

var testGOPATH = false // 用于测试的强制路径

// codeRoots返回用于搜索包的代码根。
// 在GOPATH模式中，这是GOROOT/src和GOPATH/src，具有空的导入路径。
// 在模块模式下，这是每个模块根目录，导入路径设置为其模块路径。
func codeRoots() []Dir {
	codeRootsCache.once.Do(func() {
		codeRootsCache.roots = findCodeRoots()
	})
	return codeRootsCache.roots
}

var codeRootsCache struct {
	once  sync.Once
	roots []Dir
}

var usingModules bool

func findCodeRoots() []Dir {
	var list []Dir
	if !testGOPATH {
		// 检查“go env GOMOD”是否使用了模块，
		// 如果模块已启用，则报告go.mod文件路径。
		stdout, _ := exec.Command("go", "env", "GOMOD").Output()
		gomod := string(bytes.TrimSpace(stdout))

		usingModules = len(gomod) > 0
		if usingModules {
			list = append(list,
				Dir{dir: filepath.Join(buildCtx.GOROOT, "src"), inModule: true},
				Dir{importPath: "cmd", dir: filepath.Join(buildCtx.GOROOT, "src", "cmd"), inModule: true})
		}

		if gomod == os.DevNull {
			// 模块已启用，但工作目录位于任何模块之外。
			// 我们仍然可以访问std、cmd和指定为源文件的包
			// 在命令行上，但没有模块根。
			// 避免下面的“go list-m all”，因为它不起作用。
			return list
		}
	}

	if !usingModules {
		list = append(list, Dir{dir: filepath.Join(buildCtx.GOROOT, "src")})
		for _, root := range splitGopath() {
			list = append(list, Dir{dir: filepath.Join(root, "src")})
		}
		return list
	}

	// 从go列表中查找模块根目录。
	// 最终我们需要golang.org/x/tools/go/packages
	// 要处理整个文件系统搜索并成为go/packages，
	// 但是现在，枚举模块根可以让我们适应模块
	// 以尽可能少的更改插入到当前代码中。
	mainMod, vendorEnabled, err := vendorEnabled()
	if err != nil {
		return list
	}
	if vendorEnabled {
		// 将供应商目录添加到“std”前面的搜索路径中。
		// 这样，如果主模块*是*“std”，我们将识别路径
		// 在带有该前缀的前缀之前没有“vendor/”前缀。
		list = append([]Dir{{dir: filepath.Join(mainMod.Dir, "vendor"), inModule: false}}, list...)
		if mainMod.Path != "std" {
			list = append(list, Dir{importPath: mainMod.Path, dir: mainMod.Dir, inModule: true})
		}
		return list
	}

	cmd := exec.Command("go", "list", "-m", "-f={{.Path}}\t{{.Dir}}", "all")
	cmd.Stderr = os.Stderr
	out, _ := cmd.Output()
	for _, line := range strings.Split(string(out), "\n") {
		i := strings.Index(line, "\t")
		if i < 0 {
			continue
		}
		path, dir := line[:i], line[i+1:]
		if dir != "" {
			list = append(list, Dir{importPath: path, dir: dir, inModule: true})
		}
	}

	return list
}

// 以下功能源自CL 203017的x/tools/internal/imports。

type moduleJSON struct {
	Path, Dir, GoVersion string
}

var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`)

// vendorEnabled表示是否启用了自动售货。
// 灵感来源于modload/init.go中的setDefaultBuildMod
func vendorEnabled() (*moduleJSON, bool, error) {
	mainMod, go114, err := getMainModuleAnd114()
	if err != nil {
		return nil, false, err
	}

	stdout, _ := exec.Command("go", "env", "GOFLAGS").Output()
	goflags := string(bytes.TrimSpace(stdout))
	matches := modFlagRegexp.FindStringSubmatch(goflags)
	var modFlag string
	if len(matches) != 0 {
		modFlag = matches[1]
	}
	if modFlag != "" {
		// 不要重写显式“-mod=”参数。
		return mainMod, modFlag == "vendor", nil
	}
	if mainMod == nil || !go114 {
		return mainMod, false, nil
	}
	// 检查1.14的自动供应商模式。
	if fi, err := os.Stat(filepath.Join(mainMod.Dir, "vendor")); err == nil && fi.IsDir() {
		if mainMod.GoVersion != "" && semver.Compare("v"+mainMod.GoVersion, "v1.14") >= 0 {
			// Go版本至少为1.14，并且存在供应商目录。
			// 默认情况下，Set-mod=供应商。
			return mainMod, true, nil
		}
	}
	return mainMod, false, nil
}

// getMainModuleAnd114获取主模块的信息以及
// 使用的go命令为1.14+。这是需要了解的信息
// 如果应启用自动售货机。
func getMainModuleAnd114() (*moduleJSON, bool, error) {
	const format = `{{.Path}}
{{.Dir}}
{{.GoVersion}}
{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}}
`
	cmd := exec.Command("go", "list", "-m", "-f", format)
	cmd.Stderr = os.Stderr
	stdout, err := cmd.Output()
	if err != nil {
		return nil, false, nil
	}
	lines := strings.Split(string(stdout), "\n")
	if len(lines) < 5 {
		return nil, false, fmt.Errorf("unexpected stdout: %q", stdout)
	}
	mod := &moduleJSON{
		Path:      lines[0],
		Dir:       lines[1],
		GoVersion: lines[2],
	}
	return mod, lines[3] == "go1.14", nil
}
